/*
 * OREKIT-X
 * Copyright 2002-2008 CS Communication & Systemes
 * 
 * Parts of this software package have been licensed to CS
 * Communication & Systemes (CS) under one or more contributor license
 * agreements.  See the NOTICE file distributed with this work for
 * additional information.
 *  
 * This is an experimental copy of OREKIT from www.orekit.org.
 * Please use the original OREKIT from orekit.org for normal work
 * unrelated to this research project.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License.  You
 * may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.  See the License for the specific language governing
 * permissions and limitations under the License.
 */
package ore.time;

import ore.errors.OrekitException;

import java.util.Date;

/** 
 * This class represents a specific instant in time.  
 * 
 * Instances of this class are considered to be absolute in the sense
 * that each instance represents the occurrence of some event and can
 * be compared to other instances or located in <em>any</em> {@link
 * TimeScale time scale}. In other words the different locations of an
 * event with respect to two different time scale (say {@link TAIScale
 * TAI} and {@link UTCScale UTC} for example) are simply different
 * perspective related to a single object. Only one
 * <code>AbsoluteDate</code> instance is needed, both representations
 * being available from this single instance by specifying the time
 * scales as parameter when calling the ad-hoc methods.
 *
 * Since an instance is not bound to a specific time-scale, all
 * methods related to the location of the date within some time scale
 * require to provide the time scale as an argument.  Therefore it's
 * possible to define a date in one time scale and to use it in
 * another one. An example of such use is to read a date from a file
 * in UTC and write it in another file in TAI. This can be done as
 * follows.
 * 
 * <pre>
 *   DateTimeComponents utcComponents = readNextDate();
 *   AbsoluteDate date = new AbsoluteDate(utcComponents, TimeScalesFactory.getUTC());
 *   writeNextDate(date.getComponents(TimeScalesFactory.getTAI()));
 * </pre>
 *
 * Two complementary views are available.
 * <dl>
 *   <dt>Location view (mainly for input/output or conversions)</dt>
 * 
 *   <dd>Locations represent the coordinate of one event with respect
 *   to a {@link TimeScale time scale}. The related methods are {@link
 *   #AbsoluteDate(DateComponents, TimeComponents, TimeScale)}, {@link
 *   #AbsoluteDate(int, int, int, int, int, double, TimeScale)},
 *   {@link #AbsoluteDate(int, int, int, TimeScale)}, {@link
 *   #AbsoluteDate(Date, TimeScale)}, {@link #createGPSDate(int,
 *   double)}, toString(){@link #toDate(TimeScale)}, {@link
 *   #toString(TimeScale) toString(timeScale)}, {@link #toString()},
 *   and {@link #timeScalesOffset}.  </dd>
 * 
 *   <dt>Offset view (mainly for physical computation)</dt>
 * 
 *   <dd>Offsets represent either the flow of time between two events
 *   (two instances of the class) or durations. They are counted in
 *   seconds, are continuous and could be measured using only a
 *   virtually perfect stopwatch.  The related methods are {@link
 *   #AbsoluteDate(AbsoluteDate, double)}, {@link
 *   #durationFrom(AbsoluteDate)}, {@link #compareTo(AbsoluteDate)},
 *   {@link #equals(Object)} and {@link #hashCode()}.</dd>
 * 
 * </dl>
 * 
 * A few reference epochs which are commonly used in space systems have been defined. These
 * epochs can be used as the basis for offset computation. The supported epochs are:
 * {@link #JULIAN_EPOCH}, {@link #MODIFIED_JULIAN_EPOCH}, {@link #FIFTIES_EPOCH},
 * {@link #GPS_EPOCH}, {@link #J2000_EPOCH}, {@link #JAVA_EPOCH}. In addition to these reference
 * epochs, two other constants are defined for convenience: {@link #PAST_INFINITY} and
 * {@link #FUTURE_INFINITY}, which can be used either as dummy dates when a date is not yet
 * initialized, or for initialization of loops searching for a min or max date.
 * 
 * Instances of this class are immutable.
 * 
 * @author Luc Maisonobe
 * 
 * @see TimeScale
 * @see TimeStamped
 * @see ChronologicalComparator
 */
public class AbsoluteDate 
    extends Object
    implements TimeStamped
{

    /** 
     * Reference epoch for julian dates: -4712-01-01T12:00:00.
     * 
     * Both <code>java.util.Date</code> and {@link DateComponents}
     * classes follow the astronomical conventions and consider a year
     * 0 between years -1 and +1, hence this reference date lies in
     * year -4712 and not in year -4713 as can be seen in other
     * documents or programs that obey a different convention (for
     * example the <code>convcal</code> utility).
     */
    public static final AbsoluteDate JULIAN_EPOCH =
        new AbsoluteDate(DateComponents.JULIAN_EPOCH, TimeComponents.H12, TimeScalesFactory.getTT());
    /** 
     * Reference epoch for modified julian dates: 1858-11-17T00:00:00.
     */
    public static final AbsoluteDate MODIFIED_JULIAN_EPOCH =
        new AbsoluteDate(DateComponents.MODIFIED_JULIAN_EPOCH, TimeComponents.H00, TimeScalesFactory.getTT());
    /** 
     * Reference epoch for 1950 dates: 1950-01-01T00:00:00.
     */
    public static final AbsoluteDate FIFTIES_EPOCH =
        new AbsoluteDate(DateComponents.FIFTIES_EPOCH, TimeComponents.H00, TimeScalesFactory.getTT());
    /** 
     * Reference epoch for GPS weeks: 1980-01-06T00:00:00 GPS time.
     */
    public static final AbsoluteDate GPS_EPOCH =
        new AbsoluteDate(DateComponents.GPS_EPOCH, TimeComponents.H00, TimeScalesFactory.getGPS());
    /** 
     * J2000.0 Reference epoch: 2000-01-01T12:00:00 Terrestrial Time (<em>not</em> UTC).
     */
    public static final AbsoluteDate J2000_EPOCH =
        new AbsoluteDate(DateComponents.J2000_EPOCH, TimeComponents.H12, TimeScalesFactory.getTT());
    /** 
     * Java Reference epoch: 1970-01-01T00:00:00 TT.
     */
    public static final AbsoluteDate JAVA_EPOCH =
        new AbsoluteDate(DateComponents.JAVA_EPOCH, TimeComponents.H00, TimeScalesFactory.getTT());
    /** 
     * Dummy date at infinity in the past direction.
     */
    public static final AbsoluteDate PAST_INFINITY =
        new AbsoluteDate(AbsoluteDate.JAVA_EPOCH, Double.NEGATIVE_INFINITY);
    /** 
     * Dummy date at infinity in the future direction.
     */
    public static final AbsoluteDate FUTURE_INFINITY =
        new AbsoluteDate(AbsoluteDate.JAVA_EPOCH, Double.POSITIVE_INFINITY);
    /**
     * Multiply by a GPS week number for an offset to the start of the
     * week in the GPS EPOCH.
     */
    public final static double GPS_WEEK = 604800.0;
    /** 
     * GPS dates are provided as a week number starting from the
     * {@link #GPS_EPOCH GPS epoch}, and a number of milliseconds
     * since week start.
     * 
     * @param weekNumber Week number since the {@link #GPS_EPOCH GPS
     * epoch}
     * @param milliInWeek Number of milliseconds since week start
     * 
     * @return A new instant
     */
    public static AbsoluteDate createGPSDate(int weekNumber, double milliInWeek) {
        double offset = ((weekNumber * GPS_WEEK) + (milliInWeek / 1000.0));
        return new AbsoluteDate(GPS_EPOCH, offset);
    }


    /** 
     * Reference epoch in seconds from 2000-01-01T12:00:00 TAI.
     * 
     * Beware, it is not {@link #J2000_EPOCH} since it is in TAI and not in TT.
     */
    private final long epoch;
    /** 
     * Offset from the reference epoch in seconds.
     */
    private final double offset;


    /** 
     * Default epoch {@link #J2000_EPOCH}.
     */
    public AbsoluteDate() {
        super();
        this.epoch  = J2000_EPOCH.epoch;
        this.offset = J2000_EPOCH.offset;
    }
    /** 
     * @param location Location in the time scale
     * @param timeScale Time scale
     */
    public AbsoluteDate(DateTimeComponents location, TimeScale timeScale){
        this(location.getDate(), location.getTime(), timeScale);
    }
    /** 
     * @param date Date location in the time scale
     * @param time Time location in the time scale
     * @param timeScale Time scale
     */
    public AbsoluteDate(DateComponents date, TimeComponents time, TimeScale timeScale) {
        super();
        /*
         * Set the epoch at the start of the current minute
         */
        final int j2000Day = date.getJ2000Day();
        this.epoch  = (60L * ((j2000Day * 24L + time.getHour()) * 60L + time.getMinute() - 720L));
        this.offset = (time.getSecond() + timeScale.offsetToTAI(date, time));
    }
    /** 
     * @param year Year number (may be 0 or negative for BC years)
     * @param month Month number from 1 to 12
     * @param day Day number from 1 to 31
     * @param hour Hour number from 0 to 23
     * @param minute Minute number from 0 to 59
     * @param second Second number from 0.0 to 60.0 (excluded)
     * @param timeScale Time scale
     * @exception IllegalArgumentException Parameters out of range
     */
    public AbsoluteDate(int year, int month, int day, int hour, int minute, double second,
                        TimeScale timeScale)
        throws IllegalArgumentException
    {
        this (new DateComponents(year, month, day), new TimeComponents(hour, minute, second), 
             timeScale);
    }
    /** 
     * The time components are set to zero (00:00:00.000).
     * 
     * @param date Date location in the time scale
     * @param timeScale Time scale
     * @exception IllegalArgumentException Parameters out of range.
     */
    public AbsoluteDate(DateComponents date, TimeScale timeScale)
        throws IllegalArgumentException
    {
        this(date, TimeComponents.H00, timeScale);
    }
    /**
     * The time components are set to zero (00:00:00.000).
     * 
     * @param year Year number (may be 0 or negative for BC years)
     * @param month Month number from 1 to 12
     * @param day Day number from 1 to 31
     * @param timeScale Time scale
     * @exception IllegalArgumentException Parameters out of range.
     */
    public AbsoluteDate(int year, int month, int day, TimeScale timeScale)
        throws IllegalArgumentException
    {
        this(new DateComponents(year, month, day), TimeComponents.H00, timeScale);
    }
    /** 
     * Convert from the Java Epoch into the argument time scale.
     * 
     * @param location Location in the time scale
     * @param timeScale Time scale
     */
    public AbsoluteDate(Date location, TimeScale timeScale) {
        this (new DateComponents(DateComponents.JAVA_EPOCH,
                                 (int) (location.getTime() / 86400000L)),
              new TimeComponents((location.getTime() % 86400000L) / 1000.0),
              timeScale);
    }
    /** 
     * Build an instance from an actual (physical) elapsed duration
     * relative to another instant.
     * 
     * For example, the actual (physical) duration between the two
     * instants 2005-12-31T23:59:59 and 2006-01-01T00:00:00 in the
     * {@link UTCScale UTC} time scale is not one second, but two.
     * That is, a stop watch would have measured an elapsed duration
     * of 2 seconds between these two instances because a leap second
     * was introduced at the end of 2005 in this time scale.
     * 
     * This constructor is the reverse of the {@link
     * #durationFrom(AbsoluteDate)} method.
     * 
     * @param since Start instant of the measured duration
     * @param elapsedDuration Actual (physical) elapsed duration from
     * the <code>since</code> instant, as measured in a regular time
     * scale
     */
    public AbsoluteDate(AbsoluteDate since, double elapsedDuration) {
        super();
        this.epoch = (since.epoch);
        this.offset = (since.offset + elapsedDuration);
    }
    /**
     * Build an instance from an apparent clock offset with respect to
     * another instant in a specific {@link TimeScale time scale}.
     * 
     * The apparent clock offset is not an elapsed (physical)
     * duration, but the difference between two readings on a time
     * scale. 
     * 
     * For example, the apparent clock offset between
     * 2005-12-31T23:59:59 and 2006-01-01T00:00:00 in the {@link
     * UTCScale UTC} time scale is one second. However, the actual
     * (physical) elapsed duration is two seconds (as measured by a
     * stop watch) because a leap second has been introduced at the
     * end of 2005 in this time scale.
     * 
     * This constructor is the reverse of the {@link
     * #offsetFrom(AbsoluteDate, TimeScale)} method.
     * 
     * @param reference Reference instant
     * @param apparentOffset Apparent clock offset from the reference
     * instant (simple arithmetic difference between two readings)
     * @param timeScale Time scale in which the offset is defined
     */
    public AbsoluteDate(AbsoluteDate reference, double apparentOffset, TimeScale timeScale) {
        this(new DateTimeComponents(reference.getComponents(timeScale), 
                                    apparentOffset),
             timeScale);
    }


    /**
     * Compute the actual or physically elapsed duration between two
     * instants.
     * 
     * The returned duration is the number of seconds physically
     * elapsed between the two instants, measured in a regular time
     * scale with respect to surface of the Earth (i.e either the
     * {@link TAIScale TAI scale}, the {@link TTScale TT scale} or the
     * {@link GPSScale GPS scale}). It is the only method that gives
     * actual temporal duration (having a physical meaning), as
     * opposed to an apparent simple arithmetic offset not
     * incorporating real adjustments in the time scale (leaps).
     * 
     * This method gives the same result (with less computation) as
     * calling {@link #offsetFrom(AbsoluteDate, TimeScale)} with a
     * second argument set to one of the regular scales cited above.
     * 
     * This method is the reverse of the {@link
     * #AbsoluteDate(AbsoluteDate, double)} constructor.
     * 
     * @param instant Instant to subtract from the instance
     * 
     * @return Duration in seconds between the two instants (positive
     * if the instance is posterior to the argument)
     */
    public final double durationFrom(AbsoluteDate instant) {
        return ((this.epoch - instant.epoch) + (this.offset - instant.offset));
    }
    /** 
     * Compute the apparent or simple arithmetic offset between two
     * instants in the perspective of a specific {@link TimeScale time
     * scale}.
     * 
     * The offset is the number of seconds counted in the given
     * time scale between the locations of the two instants, with
     * all time scale irregularities removed (i.e. considering all
     * days are exactly 86400 seconds long). 
     * 
     * This method will give a result that will not have a physical
     * meaning where the time scale is irregular (across leaps).
     * 
     * For example since a leap second was introduced at the end of
     * 2005, the apparent offset between 2005-12-31T23:59:59 and
     * 2006-01-01T00:00:00 is 1 second, but the actual or physical
     * duration of the corresponding time interval (as returned by the
     * {@link #durationFrom(AbsoluteDate)} method) is 2 seconds.
     * 
     * This method is the reverse of the {@link
     * #AbsoluteDate(AbsoluteDate, double, TimeScale)} constructor.
     * 
     * @param instant Instant to subtract from the instance
     * @param timeScale Time scale with respect to which the offset
     * should be computed
     * 
     * @return Apparent clock offset in seconds between the two instants
     * (positive if the instance is posterior to the argument)
     */
    public final double offsetFrom(AbsoluteDate instant, TimeScale timeScale) {
        double elapsedDuration = ((this.epoch - instant.epoch) +
                                  (this.offset - instant.offset));
        double startOffset     = timeScale.offsetFromTAI(instant);
        double endOffset       = timeScale.offsetFromTAI(this);
        return (elapsedDuration - startOffset + endOffset);
    }
    /** 
     * Compute the offset between two time scales at the current instant.
     * 
     * The offset is defined as <i>l<sub>1</sub>-l<sub>2</sub></i>
     * where <i>l<sub>1</sub></i> is the location of the instant in
     * the <code>scale1</code> time scale and <i>l<sub>2</sub></i> is
     * the location of the instant in the <code>scale2</code> time
     * scale.
     * 
     * @param scale1 First time scale
     * @param scale2 Second time scale
     * 
     * @return Offset in seconds between the two time scales at the
     * current instant
     */
    public final double timeScalesOffset(TimeScale scale1, TimeScale scale2) {
        return (scale1.offsetFromTAI(this) - scale2.offsetFromTAI(this));
    }
    /** 
     * Convert the instance to a Java {@link java.util.Date Date}.
     * 
     * Conversion to the Date class induces a loss of precision
     * because the Date class does not provide sub-millisecond
     * information. Java Dates are considered to be locations in some
     * times scales.
     * 
     * @param timeScale Time scale to use
     * 
     * @return A {@link java.util.Date Date} instance representing the
     * location of the instant in the argument time scale
     */
    public final Date toDate(final TimeScale timeScale) {
        double time = this.epoch + this.offset + timeScale.offsetFromTAI(this);
        return new Date(Math.round((time + 10957.5 * 86400.0) * 1000L));
    }
    /** 
     * Split the instance into its components.
     * 
     * @param timeScale Time scale to use
     * 
     * @return Date and time components
     */
    public final DateTimeComponents getComponents(TimeScale timeScale) {
        /*
         * Compute offset from 2000-01-01T00:00:00 in specified time scale
         */
        double offset2000 = (this.epoch + this.offset + 43200.0 + timeScale.offsetFromTAI(this));
        int    day        = (int) Math.floor(offset2000 / 86400.0);
        /*
         * Components
         */
        DateComponents date = new DateComponents(DateComponents.J2000_EPOCH, day);
        TimeComponents time = new TimeComponents(offset2000 - 86400.0 * day);
        if (timeScale instanceof UTCScale){
            UTCScale utc = (UTCScale) timeScale;
            if (utc.insideLeap(this)) {
                /*
                 * Count leaps
                 */
                time = new TimeComponents(time.getHour(), time.getMinute(),
                                          (time.getSecond() + utc.getLeap(this)));
            }
        }
        return new DateTimeComponents(date, time);
    }
    public final int compareTo(TimeStamped stamped) {
        return this.compareTo(stamped.getDate());
    }
    /** 
     * @param date Another date to compare the instance to
     * 
     * @return A negative integer, zero, or a positive integer as this
     * date is before, simultaneous, or after the specified date.
     */
    public final int compareTo(AbsoluteDate date) {
        double delta = this.durationFrom(date);
        if (delta < 0)
            return -1;
        else if (delta > 0)
            return +1;
        else
            return 0;
    }
    /**
     * {@inheritDoc}
     */
    public final AbsoluteDate getDate() {
        return this;
    }
    /** 
     * Check if the instance represent the same time as another instance.
     * 
     * @param date Another date
     * 
     * @return True if the instance and the other date refer to the same instant
     */
    public boolean equals(final Object date) {
        if (date instanceof AbsoluteDate)
            return (0.0 == this.durationFrom((AbsoluteDate) date));
        else
            return false;
    }
    public int hashCode() {
        long l = Double.doubleToLongBits(this.durationFrom(J2000_EPOCH));
        return (int) (l ^ (l >>> 32));
    }
    /** 
     * @return ISO-8601 format with milliseconds accuracy
     */
    public String toString() {
        try {
            return toString(TimeScalesFactory.getUTC());
        }
        catch (OrekitException oe) {
            throw new RuntimeException(oe);
        }
    }
    /** 
     * @return ISO-8601 format with milliseconds accuracy
     */
    public String toString(TimeScale timeScale) {
        return this.getComponents(timeScale).toString();
    }
}
